Repository navigation
feat(release): publish the yank list weekly, serve both lists, attach them to releases (#226, part 2) - #237
Merged
Merged
Conversation
Madraka
force-pushed
the
claude/release-trust-226-publish
branch
from
November 14, 2026 14:55
970f9b4 to
a35034d
Compare
Madraka
force-pushed
the
claude/release-trust-226-publish
branch
from
November 14, 2026 15:17
a35034d to
1aa446c
Compare
Madraka
added a commit
that referenced
this pull request
Nov 14, 2026
…fied pair (#226) - sign-yanked verifies the published yanked.json (and .sig) as a consumer would before carrying anything forward, and fails loudly otherwise. --rotate starts a new yank key's list from the source at serial 1; --accept-expired carries forward a list that expired in an outage, still verified. The yank-list logic moves to scripts/release-keys/yank-list.ts. - Emergency entries are comma-separated and validated (at most 20 a run). - yank-list.yml: a check job skips before security/keys.json exists and outside cloudhal-dev/cloudhal; inputs go as --name=value; rotate and accept_expired inputs; the push uses a deploy key held by release-yank (the ruleset's only bypass), so every token is read-only; a job summary and an issue on a failed or cancelled run. - The docs Worker serves yanked.json and .sig only as a pair whose signature verifies with the static keys.json's yank key, cached as one entry; the last good pair or 502 on a mismatch, never a mismatch. - publish-ci's consumer check applies mergeTrust with the verified key and yank lists; trustAssets returns both. - Runbook: deploy-key ruleset and remaining exposure, yank-key rotation, multi-entry dispatches, merge step 6 with no release pending. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
Madraka
force-pushed
the
claude/release-trust-226-publish
branch
from
November 14, 2026 15:29
1aa446c to
a392d88
Compare
Madraka
added a commit
that referenced
this pull request
Nov 14, 2026
…fied pair (#226) - sign-yanked verifies the published yanked.json (and .sig) as a consumer would before carrying anything forward, and fails loudly otherwise. --rotate starts a new yank key's list from the source at serial 1; --accept-expired carries forward a list that expired in an outage, still verified. The yank-list logic moves to scripts/release-keys/yank-list.ts. - Emergency entries are comma-separated and validated (at most 20 a run). - yank-list.yml: a check job skips before security/keys.json exists and outside cloudhal-dev/cloudhal; inputs go as --name=value; rotate and accept_expired inputs; the push uses a deploy key held by release-yank (the ruleset's only bypass), so every token is read-only; a job summary and an issue on a failed or cancelled run. - The docs Worker serves yanked.json and .sig only as a pair whose signature verifies with the static keys.json's yank key, cached as one entry; the last good pair or 502 on a mismatch, never a mismatch. - publish-ci's consumer check applies mergeTrust with the verified key and yank lists; trustAssets returns both. - Runbook: deploy-key ruleset and remaining exposure, yank-key rotation, multi-entry dispatches, merge step 6 with no release pending. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
… them to releases (#226, part 2) PM decision A, with its conditions: - .github/workflows/yank-list.yml: weekly (Mondays) and on demand (yank, revoke, reason, first_run). - It signs security/yank-list.json, plus every published entry (carried forward), plus an emergency entry, with serial + 1. - It commits yanked.json(.sig) to the append-only `yank-list` branch: never force-pushed, contents: write on its one job, release-yank environment, actions pinned by SHA, no install, inputs only through env, and the key handed to one step. - scripts/release-keys.ts sign-yanked: --yank/--revoke/--reason (validated, with the reason required); the merge carries published entries forward; a first run without a published file works. - cloudhal.dev: - the Worker serves /.well-known/cloudhal/yanked.json(.sig) by fetching exactly the branch's raw file from a fixed URL, cached for at most 5 min, bytes unchanged, 502 on any upstream failure; - postbuild copies security/keys.json(.sig) to /.well-known/cloudhal/ once it exists, and _headers adds CORS and the .sig type. - publish-ci attaches keys.json, yanked.json and their .sig to each release, verified first (none before the root ceremony; a resumed run of a published release reuses what it carries). RFC-0046 §1 lists them. - Runbook: the yank-list ruleset, the first run, the one manual docs deploy, and emergency yank or revoke through the workflow inputs. Refs #226 Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
- yank-list.yml installs exactly @cloudhal/release-verify's pinned dependencies, with --ignore-scripts and before the key reaches any step, so sign-yanked verifies the exact bytes it signs (keys.json against the pinned root, the published list as memory). The test allows only that install. - trustAssets returns the verified yank list too. publish-ci now runs the consumer check of the bundle after it, with that list, so a yanked version or a key the list revokes is refused before anything is uploaded. - The trust-asset tests use the async signers. Refs #226 Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
…fied pair (#226) - sign-yanked verifies the published yanked.json (and .sig) as a consumer would before carrying anything forward, and fails loudly otherwise. --rotate starts a new yank key's list from the source at serial 1; --accept-expired carries forward a list that expired in an outage, still verified. The yank-list logic moves to scripts/release-keys/yank-list.ts. - Emergency entries are comma-separated and validated (at most 20 a run). - yank-list.yml: a check job skips before security/keys.json exists and outside cloudhal-dev/cloudhal; inputs go as --name=value; rotate and accept_expired inputs; the push uses a deploy key held by release-yank (the ruleset's only bypass), so every token is read-only; a job summary and an issue on a failed or cancelled run. - The docs Worker serves yanked.json and .sig only as a pair whose signature verifies with the static keys.json's yank key, cached as one entry; the last good pair or 502 on a mismatch, never a mismatch. - publish-ci's consumer check applies mergeTrust with the verified key and yank lists; trustAssets returns both. - Runbook: deploy-key ruleset and remaining exposure, yank-key rotation, multi-entry dispatches, merge step 6 with no release pending. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
When the published list is signed by another yank key than --key-id, it is verified with that key as the newer keys.json still lists it (revoked or retiring), as of its own issuedAt; every entry is carried forward and the serial continues. Its memory names no keys.json (the epoch), so the new key's first list passes the self-check. A crafted list fails the old key's signature; an unlisted signer is refused. Only --rotate prunes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
Madraka
force-pushed
the
claude/release-trust-226-publish
branch
from
November 14, 2026 15:36
a392d88 to
c2e4149
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Owner steps (after #235's ceremony steps 1 to 4; full commands in the runbook › First-time setup)
security/keys.json, its.sigandsecurity/keys.source.jsonin a reviewed PR.yank-list. Generate an Ed25519 deploy key (ssh-keygen -t ed25519 -N "" -C yank-list), add its public half as a deploy key with write access, and set the private half as therelease-yanksecretYANK_LIST_DEPLOY_KEY. Then, in the repository settings › Rules, add a branch ruleset foryank-list: block force pushes, restrict deletions, and restrict updates with theyank-listdeploy key as the only bypass (not "GitHub Actions"). Mergesecurity/keys.jsononly while no release is pending, and do this step and the next ones straight after (runbook, steps 6–9).first_runchecked. It creates theyank-listbranch at serial 1, and from then on it runs every Monday.pnpm --filter @cloudhal/docs run deploy. This ships the/.well-known/cloudhal/yanked.jsonroute and the statickeys.json. Nothing deploys cloudhal.dev automatically yet (cloudhal.dev: deploy on push to main (RFC-0037) #236), so redeploy after everysecurity/keys.jsonchange too.Nothing here generates keys, creates environments or secrets, or runs a workflow.
What (PM decision A, with its conditions)
.github/workflows/yank-list.yml. It runs weekly (Mondays 04:17 UTC) and onworkflow_dispatchwithyank,revoke,reasonandfirst_run.permissions: {}, read-only tokens (the push usesrelease-yank's deploy key),issues: writeonly on the failure notice. Actions are pinned by SHA.@cloudhal/release-verify's pinned dependencies, with--ignore-scripts, before the key is used. That letssign-yankedverify the exact bytes it signs.RELEASE_YANK_KEYreaches the Sign step only, which runsscripts/release-keys.ts, Node built-ins only.env(no${{ }}in anyrun), and the script validates them: a version or key id pattern, and a reason that is required with at most 500 characters.yank-listbranch, signs, and pushes one new commit. It never force-pushes.test/yank-list-workflow.test.tschecks all of this.sign-yanked. The list it signs is thesecurity/yank-list.jsonsource, plus every entry of the published list (carried forward, so a yank or revocation is never dropped), plus the emergency entry.serialis the published serial plus one (1 on the first run).apps/docs/src/worker/trust.ts). It serves exactly/.well-known/cloudhal/yanked.jsonand.sig.raw.githubusercontent.com/cloudhal-dev/cloudhal/yank-list/<file>, with no part of the request in it.cf.cacheTtl300, andCache-Control: max-age=300.application/jsonorapplication/octet-stream.no-store, never an old copy. Only GET and HEAD are allowed.keys.jsonis a static asset:postbuildcopiessecurity/keys.json(.sig)to/.well-known/cloudhal/once they exist, and_headersadds CORS and the.sigtype.publish-ciattacheskeys.json,yanked.jsonand their.sig, verified with release-verify before upload (scripts/release-trust-assets.ts).--planstill runs without an install (checked against live npm).serialandlastSerialare in feat(release): signed key list, yank list and key expiry (#226, part 1) #235): the branch is append-only under the ruleset. RFC §9 and the runbook say that a stateless Worker consumer can't detect a rollback inside the 30 days.security/yank-list.jsonafterwards; published entries are carried forward anyway).maindeployscloudhal-dev" is cloudhal.dev: deploy on push to main (RFC-0037) #236. It is not built here.Rebased onto #235 round 1 (a35034d)
trustAssetsreturns the verified yank list, andpublish-ciruns the consumer check of the bundle with it, before any upload.keys.jsoncommitted before the first yank list) with this PR's steps.release-trust-assets4,yank-list-workflow4,release-keys14, and publish, plan, workflows, sign and verify all green. Docs 39.--plangivespublish=false, and biome, size and drift are clean.Review round (1aa446c, rebased onto #235 6902752)
scripts/release-keys/yank-list.ts):keys.json, the schema and freshness;--rotate(dispatch inputrotate) starts a new yank key's list from the source at serial 1;--accept-expiredcarries forward a list that expired during an outage, still verified as of its issue.release-yank(YANK_LIST_DEPLOY_KEY, Publish step only, pinned SSH host key). It is the ruleset's only bypass, so no job token hascontents: write. The runbook states what remains exposed.keys.json, cached as one entry for 5 minutes;etagandcache-control: no-cache.publish-ci:trustAssetsreturns the verified keys too, andverifyAsConsumerappliesmergeTrust(pinned, { keys, yanked }). A key revoked in either list, or a yanked version, blocks the publish (tested).--name=value;notifyjob (issues: writeonly) that opens or comments on "Yank list run failed" when a run fails or is cancelled.checkjob skips with a notice outsidecloudhal-dev/cloudhaland beforesecurity/keys.jsonexists, so the environment is not even referenced before then;release-keys19,release-trust-assets4,yank-list-workflow6,release-bundle-publish10,release-publish-plan17,release-plan9,release-workflows14,release-bundle-sign5,release-bundle-verify12,release-bundle18;--plangivespublish=false.Tests (run locally, one at a time)
apps/docs: 39 passed, including the newtrust.test.ts(4): fixed URLs that ignore the request's query, the cache settings, bytes passed through, 502 on 404, 503, a thrown fetch or an oversized body, GET and HEAD only, and the Worker routing only the two paths. Typecheck is clean, and the docs build passes (25 schemas, no.well-knownbefore the ceremony).packages/cli:release-keys10 (merging, emergency entries, input validation, serial);release-trust-assets4 (none before the ceremony; attach verified; refuse unverifiable or unreachable; a published release keeps its own);yank-list-workflow4;release-bundle-publish9,release-publish-plan17,release-plan9,release-workflows14 andrelease-bundle-sign5.publish-ci --planruns; biome,size:checkand the cli typecheck are clean.Closes #226
🤖 Generated with Claude Code